package com.zerro.litez.compiler.processor.file_builder;

import com.squareup.javapoet.*;
import com.zerro.litez.compiler.processor.parser.EntityClassParser;
import com.zerro.litez.compiler.processor.parser.FieldInfo;

import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import java.sql.SQLException;
import java.util.*;


public class FormatterClassBuilder extends FileBuilder<EntityClassParser> {

    String entity = "entity";
    String index = "index";
    String resultSet = "resultSet";
    String notNullIndexes = "notNullIndexes";
    ParameterSpec resultSetParam = ParameterSpec.builder(ClassName.get("java.sql", "ResultSet"), "resultSet").build();

    public FormatterClassBuilder(EntityClassParser parser, Element e) {
        super(parser, e.getSimpleName() + "_Formatter", e);
        onBuilding();
        finish();
    }

    @Override
    protected void onBuilding() {

        findCycleLinkedFieldInfos();

        CodeBlock.Builder notNullsInit = CodeBlock.builder();
        Collection<FieldInfo> accessibleFieldInfoList = parser.getAllInfoOfField(3);
        Iterator<FieldInfo> infoIterator = accessibleFieldInfoList.iterator();
        int i = 0;
        while (infoIterator.hasNext()) {
            FieldInfo fieldInfo = infoIterator.next();
            if (!fieldInfo.isNullable()) {
                notNullsInit.addStatement("$L.add($L)", notNullIndexes, i);
            }
            i++;
        }
        getClassBuilder().superclass(superClass())
                .addField(FieldSpec.builder(ParameterizedTypeName.get(List.class, Integer.class),
                                notNullIndexes, Modifier.PRIVATE, Modifier.STATIC)
                        .initializer("new $T<>()", ArrayList.class)
                        .build())
                .addMethod(constructor())
                .addMethod(valuesOf())
                .addMethod(resultSetToEntity())
                .addMethod(getPrimaryKeyValue())
                .addMethod(getValueOrDefaultOf())
                .addMethod(getColumnCount())
                .addMethod(getAutoIncIndex())
                .addMethod(setGeneratedKeysValue())
                .addMethod(getNameOf())
                .addStaticBlock(notNullsInit.build());

    }

    private final List<FieldInfo> cycleLinkedFieldInfos = new ArrayList<>();

    private void findCycleLinkedFieldInfos() {
        List<FieldInfo> allInfoOfField = parser.getAllInfoOfField(4);
        for (FieldInfo fieldInfo : allInfoOfField) {
            if (fieldInfo.isEntityType()) {
                for (FieldInfo info : fieldInfo.getEntityTypeParser().getAllInfoOfField(5)) {
                    if (info.isEntityType()) {
                        if (info.getEntityTypeParser().equals(parser) &&
                                !cycleLinkedFieldInfos.contains(info.getEntityTypePk())) {
                            cycleLinkedFieldInfos.add(info.getEntityTypePk());
                            break;
                        }
                    }
                }
            }
        }

    }


    public MethodSpec constructor() {
        MethodSpec.Builder method = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PROTECTED)
                .addStatement("formatterMap.put($S, this)", parser.getParsingElement().getQualifiedName().toString());
        for (FieldInfo info : cycleLinkedFieldInfos) {
            method.addStatement("queriedPrimaryKeysEntities.put($S, new $T<>())", info.getName(), ArrayList.class);
        }
        return method.build();
    }


    private ParameterizedTypeName superClass() {
        return ParameterizedTypeName.get(
                ClassName.get(runtimePackage, "EntityFormatter"),
                ClassName.get(parser.getElementPackage(), e.getSimpleName().toString()));
    }

    private MethodSpec getValueOrDefaultOf() {

        CodeBlock.Builder code = CodeBlock.builder()
                .addStatement("$T v = null", String.class)
                .beginControlFlow("switch ($L)", index);

        List<FieldInfo> infoList = parser.getAllInfoOfField(6);
        for (int i = 0; i < infoList.size(); i++) {
            FieldInfo info = infoList.get(i);
            CodeBlock.Builder caseCode = CodeBlock.builder().indent();
            String getter = entity + "." + info.getGetter();
            if (!info.getDefaultValue().isEmpty()) {
                getter = getter + " == null ? \"" +
                        info.getDefaultValue() + "\":" + getter;
            } else if (!info.isNullable()) {
                caseCode.beginControlFlow("if($L.$L == null)", entity, info.getGetter())
                        .addStatement("throwNotnull($S,$S)", e.toString(), info.getName())
                        .endControlFlow();
            }
            if (info.isArrayType() || info.isContainer()) {
                String type = info.isArrayType() ? "array" : "list";
                if (!info.isEntityType()) {
                    if (info.isContainer()) {
                        type = "simpleList";
                    }
                    caseCode.addStatement("v = $S + escape($LToString($L)) + $S", "'", type, getter, "'");
                } else {
                    List<FieldInfo> fieldInfoList = info.getEntityTypeParser().getAllInfoOfField(7);
                    int index = 0;
                    for (int j = 0; j < fieldInfoList.size(); j++) {
                        FieldInfo fieldInfo = fieldInfoList.get(j);
                        if (fieldInfo.getName().equals(info.getEntityTypePk().getName())) {
                            index = j;
                            break;
                        }
                    }
                    caseCode.addStatement("v = $S + escape($LToString($L, $S, $L)) + $S",
                            "'", type, getter, info.getEntityTypeParser().getParsingElement().getQualifiedName(), index, "'");
                }
            } else if (!info.isSimpleType()) {
                caseCode.beginControlFlow("if($L != null)", getter);

                if (info.isEnumType()) {
                    caseCode.addStatement("v = $S + escape($L) + $S",
                            "'", getter + ".name()", "'");
                } else if (info.isEntityType()) {
                    if (info.getEntityTypePk().getRealType().equals(String.class.getName())) {
                        getter = getter + '.' + info.getEntityTypePk().getGetter();
                        caseCode.addStatement("v = $S + escape($L) + $S", "'", getter, "'");
                    } else {
                        caseCode.addStatement("v = $T.valueOf($L)", String.class, getter);
                    }
                }
                caseCode.addStatement("break")
                        .endControlFlow()
                        .addStatement("v = $L", "null");
            } else if (info.getRealType().equals(String.class.getName())) {
                caseCode.addStatement("v = $S + escape($L) + $S", "'", getter, "'");
            } else {
                caseCode.addStatement("v = $T.valueOf($L)", String.class, getter);
            }

            CodeBlock aCase = getCase("$L", i, caseCode.unindent().build(), true);
            code.add(aCase);
        }
        code.endControlFlow()
                .beginControlFlow("if (v == null && $L.contains($L))", notNullIndexes, index)
                .addStatement("throwNotnull($S, getNameOf($L))", e.toString(), index)
                .endControlFlow()
                .addStatement("return v");

        return MethodSpec.methodBuilder("getValueOrDefaultOf")
                .addModifiers(Modifier.PROTECTED)
                .returns(String.class)
                .addParameter(buildEntityClass(), entity)
                .addParameter(int.class, index)
                .addCode(code.build())
                .build();
    }

    private MethodSpec getNameOf() {
        CodeBlock.Builder code = CodeBlock.builder()
                .beginControlFlow("switch ($L)", index);

        List<FieldInfo> accessibleFieldInfoList = parser.getAllInfoOfField(8);
        for (int i = 0; i < accessibleFieldInfoList.size(); i++) {
            FieldInfo info = accessibleFieldInfoList.get(i);

            CodeBlock caseCode = CodeBlock.builder()
                    .addStatement("return $S", info.getColumnName()).build();
            code.add(getCase("$L", i, caseCode, false));
        }
        code.add("default:\n\t")
                .addStatement("return null");
        code.endControlFlow();

        return MethodSpec.methodBuilder("getNameOf")
                .addModifiers(Modifier.PROTECTED)
                .returns(String.class)
                .addParameter(int.class, index)
                .addCode(code.build())
                .build();
    }

    private MethodSpec getColumnCount() {
        int c = parser.getAllInfoOfField(9).size();
        return MethodSpec.methodBuilder("getColumnCount")
                .addModifiers(Modifier.PROTECTED)
                .returns(int.class)
                .addStatement("return $L", c)
                .build();
    }


    private MethodSpec getAutoIncIndex() {
        Integer index = null;
        List<FieldInfo> allInfoOfField = parser.getAllInfoOfField(10);
        for (int i = 0; i < allInfoOfField.size(); i++) {
            if (allInfoOfField.get(i).equals(parser.getAutoInc())) {
                index = i;
            }
        }
        return MethodSpec.methodBuilder("getAutoIncIndex")
                .addModifiers(Modifier.PROTECTED)
                .returns(Integer.class)
                .addStatement("return $L", index)
                .build();
    }


    private MethodSpec valuesOf() {

        CodeBlock.Builder valuesStrArr = CodeBlock.builder()
                .add("$S ", "(");
        List<FieldInfo> accessibleFieldInfoList = parser.getAllInfoOfField(11);
        int size = accessibleFieldInfoList.size();
        for (int j = 0; j < size; j++) {
            FieldInfo fieldInfo = accessibleFieldInfoList.get(j);
            if (fieldInfo.equals(parser.getAutoInc())) {
                continue;
            }

            valuesStrArr.add(" + getValueOrDefaultOf($L, $L)", entity, j);
            if (j != size - 1) {
                valuesStrArr.add(" + $S", ", ");
            }
        }
        valuesStrArr.addStatement(" + $S", ")");

        return MethodSpec.methodBuilder("valuesOf")
                .addModifiers(Modifier.PROTECTED)
                .returns(String.class)
                .addParameter(buildEntityClass(), entity)
                .addCode("return $L", valuesStrArr.build().toString())
                .build();
    }

    private MethodSpec resultSetToEntity() {

        CodeBlock.Builder setter = CodeBlock.builder();
        String[] builderFields = new String[parser.getBuilderParamCount()];

        List<FieldInfo> allFieldInfoList = parser.getAllInfoOfField(12);
        List<FieldInfo> fieldInfoList = new ArrayList<>(allFieldInfoList);

        boolean builderHasField = builderFields.length != 0;
        if (!builderHasField) {
            setter.addStatement("$T $L = new $T()", buildEntityClass(), entity, buildEntityClass());
        }
        List<FieldInfo> removeInfo = new ArrayList<>();
        for (FieldInfo fieldInfo : fieldInfoList) {
            if (fieldInfo.getConstructorIndex() != -1) {
                buildSetter(fieldInfo, setter, allFieldInfoList.indexOf(fieldInfo), builderHasField);
                if (fieldInfo.getConstructorIndex() >= builderFields.length) {
                    break;
                }
                builderFields[fieldInfo.getConstructorIndex()] = fieldInfo.getName();
                removeInfo.add(fieldInfo);
            }
        }
        if (builderHasField) {
            setter.addStatement("$T $L = new $T($L)",
                    buildEntityClass(), entity, buildEntityClass(), String.join(", ", builderFields));
            builderHasField = false;
        }
        fieldInfoList.removeAll(removeInfo);
        for (FieldInfo fieldInfo : removeInfo) {
            if (cycleLinkedFieldInfos.contains(fieldInfo)) {
                setter.addStatement("queriedPrimaryKeysEntities.get($S).add($L)", fieldInfo.getName(), entity);
            }
        }
        removeInfo.clear();

        for (FieldInfo fieldInfo : fieldInfoList) {
            if (cycleLinkedFieldInfos.contains(fieldInfo)) {
                buildSetter(fieldInfo, setter, allFieldInfoList.indexOf(fieldInfo), builderHasField);

                setter.addStatement("queriedPrimaryKeysEntities.get($S).add($L)", fieldInfo.getName(), entity);
                removeInfo.add(fieldInfo);
            }
        }
        fieldInfoList.removeAll(removeInfo);
        removeInfo.clear();

        for (FieldInfo fieldInfo : fieldInfoList) {
            buildSetter(fieldInfo, setter, allFieldInfoList.indexOf(fieldInfo), builderHasField);
        }


        return MethodSpec.methodBuilder("resultSetToEntity")
                .addModifiers(Modifier.PROTECTED)
                .returns(buildEntityClass())
                .addParameter(resultSetParam)
                .addParameter(ClassName.get(runtimePackage, "LitEzSupporter"), "supporter")
                .beginControlFlow("try")
                .addCode(setter.build())
                .addStatement("clearQueried()")
                .addStatement("return $L", entity)
                .endControlFlow()
                .beginControlFlow("catch ($T $L)", SQLException.class, "e")
                .addStatement("throw new $T($L)", RuntimeException.class, "e")
                .endControlFlow()
                .build();
    }

    private void buildSetter(FieldInfo fieldInfo, CodeBlock.Builder setter, int i, boolean builderHasField) {

        if (fieldInfo.isEnumType()) {
            setter.add(onEnumType(fieldInfo, i, !builderHasField));
        } else if (fieldInfo.isEntityType()) {
            setter.add(onEntityType(fieldInfo, i, !builderHasField));
        } else if (fieldInfo.isSimpleType()) {
            setter.add(onNormalType(fieldInfo, i, !builderHasField));
        }
    }

    private static String getGetType(String fieldType) {
        String[] splitType = fieldType.split("\\.");
        String getType = splitType[splitType.length - 1];
        if (fieldType.equals(Integer.class.getName())) {
            getType = "Int";
        }
        return getType;
    }

    private MethodSpec getPrimaryKeyValue() {

        List<FieldInfo> primaryKeys = parser.getPrimaryKeys();
        StringBuilder value = new StringBuilder("null");
        if (!primaryKeys.isEmpty()) {

            value = new StringBuilder("\"");
            List<FieldInfo> allInfoOfField = parser.getAllInfoOfField(13);
            for (int i = 0; i < primaryKeys.size(); i++) {
                FieldInfo pkInfo = primaryKeys.get(i);
                String pk = pkInfo.getColumnName();
                value.append(pk).append(" = \" + ")
                        .append("getValueOrDefaultOf(")
                        .append(entity).append(", ")
                        .append(allInfoOfField.indexOf(pkInfo)).append(")");
                if (i != primaryKeys.size() - 1) {
                    value.append(" + ").append("\" and ");
                }
            }
        }

        return MethodSpec.methodBuilder("getPrimaryKeyValue")
                .addModifiers(Modifier.PROTECTED)
                .returns(String.class)
                .addParameter(buildEntityClass(), entity)
                .addStatement("return $L", value.toString())
                .build();
    }

    private MethodSpec setGeneratedKeysValue() {
        FieldInfo autoInc = parser.getAutoInc();
        CodeBlock.Builder code = CodeBlock.builder();
        if (autoInc != null) {
            CodeBlock valueCode = CodeBlock.builder()
                    .add("$L.get$L(getAutoIncIndex() + 1)",
                            resultSet, getGetType(autoInc.getRealType()))
                    .build();
            code.beginControlFlow("if ($L.first())", resultSet)
                    .addStatement("$L.$L",
                            entity, autoInc.getSetter(valueCode.toString()))
                    .endControlFlow();
        }
        return MethodSpec.methodBuilder("setGeneratedKeysValue")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(buildEntityClass(), entity)
                .addParameter(resultSetParam)
                .addException(SQLException.class)
                .addCode(code.build())
                .build();
    }


    private CodeBlock getCase(String type, Object caseValue, CodeBlock caseCode, boolean break_) {
        return CodeBlock.builder()
                .add("case " + type + ":\n\t", caseValue)
                .add(caseCode)
                .add(break_ ? "\tbreak;\n" : "")
                .build();
    }

    private ClassName buildEntityClass() {
        return ClassName.get(parser.getElementPackage(), e.getSimpleName().toString());
    }

    private CodeBlock findColumn(String name) {
        return CodeBlock.builder()
                .add("$L.findColumn($S)", resultSet, name)
                .build();
    }

    private CodeBlock onEnumType(FieldInfo fieldInfo, int index, boolean builtEntity) {

        String nullOr = "";
        if (fieldInfo.isNullable()) {
            nullOr = '_' + fieldInfo.getName() + " == null ? null :";
        }
        CodeBlock.Builder code = CodeBlock.builder();
        String getType = getGetType(String.class.getName());
        CodeBlock setter = buildEnumSetter(fieldInfo, '_' + fieldInfo.getName());
        code.addStatement("String _$L = $L.get$L($L)",
                fieldInfo.getName(),
                resultSet, getType, findColumn(fieldInfo.getName()));
        if (fieldInfo.isArrayType() || fieldInfo.isContainer()) {

            code.add(onArrayOrContainerType(getEnumClass(fieldInfo), '_' + fieldInfo.getName(), fieldInfo,
                    buildEnumSetter(fieldInfo, "value")));
            if (builtEntity) {
                code.addStatement("$L.$L", entity, fieldInfo.getSetter(fieldInfo.getName()));
            }
            return code.build();
        }
        if (builtEntity) {
            String finalSetter = CodeBlock.builder()
                    .add("$L $L", nullOr, setter)
                    .build().toString();
            code.addStatement("$L.$L",
                            entity, fieldInfo.getSetter(finalSetter))
                    .build();
        } else {
            code.addStatement("$T $L = $L $T.valueOf(_$L)",
                    getEnumClass(fieldInfo), fieldInfo.getName(), nullOr,
                    getEnumClass(fieldInfo), fieldInfo.getName());
        }

        return code.build();
    }

    private CodeBlock onArrayOrContainerType(ClassName className, String setGetter,
                                             FieldInfo fieldInfo, CodeBlock valueSetter) {
        if (fieldInfo.isArrayType()) {
            return onArrayType(className, fieldInfo.getName(), setGetter, valueSetter);
        } else {
            return onContainerType(fieldInfo.getContainerType(), className, fieldInfo.getName(),
                    setGetter, valueSetter);
        }
    }

    private CodeBlock buildEnumSetter(FieldInfo fieldInfo, String name) {
        return CodeBlock.builder()
                .add("$T.valueOf($L)", getEnumClass(fieldInfo), name)
                .build();
    }

    private CodeBlock onEntityType(FieldInfo fieldInfo, int index, boolean builtEntity) {
        CodeBlock.Builder code = CodeBlock.builder();
        String entityTypeName = fieldInfo.getEntityTypeParser().getEntityFullName();
        FieldInfo pk = fieldInfo.getEntityTypePk();
        String pkValue = fieldInfo.getName() + '_' + pk.getName();
        String pkType = pk.getRealType();
        if (fieldInfo.isArrayType()) {
            pkType = String.class.getName();
        }
        code.addStatement("$T $L = $L", getNormalClass(pkType), pkValue,
                getResultGetter(getGetType(pkType), fieldInfo));

        int indexOfLink = fieldInfo.getEntityTypeParser().getAllInfoOfField(77).indexOf(pk);


        CodeBlock setter = buildEntityGetter(entityTypeName, pk, pkValue, indexOfLink);

        CodeBlock value = CodeBlock.builder()
                .add("$L == null ? null : $L", pkValue, setter)
                .build();

        if (fieldInfo.isArrayType() || fieldInfo.isContainer()) {

            code.add(onArrayOrContainerType(getEntityClass(fieldInfo), pkValue, fieldInfo,
                    buildEntityGetter(entityTypeName, pk, "value", indexOfLink)));
            if (builtEntity) {
                code.addStatement("$L.$L", entity, fieldInfo.getSetter(fieldInfo.getName()));
            }
            return code.build();
        }

        if (fieldInfo.isNullable()) {

            if (builtEntity) {
                code.addStatement("$L.$L", entity, fieldInfo.getSetter(value.toString()));
            } else {
                code.addStatement("$T $L = $L", getEntityClass(fieldInfo), fieldInfo.getName(), value);
            }
        } else {
            code.addStatement("$L.$L", entity, fieldInfo.getSetter(setter.toString()));
        }
        return code.build();
    }


    private CodeBlock buildEntityGetter(String entityTypeName, FieldInfo pkInfo, String value, int index) {
        return CodeBlock.builder()
                .add("selectEntity(supporter, $S, $S, $L.toString(), $L, $L)",
                        entityTypeName, pkInfo.getName(), value,
                        pkInfo.getRealType().equals(String.class.getName()), index)
                .build();
    }


    private CodeBlock onNormalType(FieldInfo fieldInfo, int index, boolean builtEntity) {
        CodeBlock.Builder code = CodeBlock.builder();

        String getType = getGetType(fieldInfo.getRealType());
        if (fieldInfo.isArrayType()) {
            getType = getGetType(String.class.getName());
        }
        CodeBlock getter = getResultGetter(getType, fieldInfo);
        ClassName normalClass = getNormalClass(fieldInfo.getRealType());
        if (fieldInfo.isArrayType() || fieldInfo.isContainer()) {
            CodeBlock setter = CodeBlock.builder()
                    .add("$T.valueOf($L)", normalClass, "value")
                    .build();
            code.add(onArrayOrContainerType(normalClass, getter.toString(), fieldInfo, setter));

            if (builtEntity) {
                code.addStatement("$L.$L", entity, fieldInfo.getSetter(fieldInfo.getName()));
            }
            return code.build();
        }

        if (builtEntity) {
            code.addStatement("$L.$L", entity, fieldInfo.getSetter(getter.toString()));
        } else {
            code.addStatement("$T $L = $L", normalClass, fieldInfo.getName(), getter);

        }
        return code.build();
    }

    private CodeBlock getResultGetter(String getType, FieldInfo fieldInfo) {
        return CodeBlock.builder()
                .add("$L.get$L($L)",
                        resultSet, getType, findColumn(fieldInfo.getName()))
                .build();
    }

    private ClassName getEntityClass(FieldInfo fieldInfo) {
        return ClassName.get(fieldInfo.getEntityTypeParser().getElementPackage(),
                fieldInfo.getEntityTypeParser().getEntityName());
    }

    private ClassName getEnumClass(FieldInfo fieldInfo) {
        return ClassName.get(fieldInfo.getEnumFullName()[0], fieldInfo.getEnumFullName()[1]);
    }

    private ClassName getNormalClass(String type) {
        String[] split = type.split("\\.");

        return ClassName.get("java.lang", split[split.length - 1]);
    }

    private CodeBlock onArrayType(ClassName className, String name, String resultSetGetter, CodeBlock valueSetter) {
        String strValue = "str_" + name;

        CodeBlock.Builder code = CodeBlock.builder();
        if (resultSetGetter != null) {
            code.addStatement("String[] $L= parseStrArray($L)", strValue, resultSetGetter);
        }
        code.addStatement("$T[] $L", className, name)

                .beginControlFlow("if ($L == null)", strValue)
                .addStatement("$L = null", name)
                .nextControlFlow("else")

                .addStatement("$L = new $T[$L.length]", name, className, strValue)
                .beginControlFlow("for(int i = 0; i < $L.length; i++)", strValue)
                .addStatement("String value = $L[i]", strValue)
                .addStatement("$L[i] = $L", name, valueSetter)
                .endControlFlow()
                .endControlFlow();

        return code.build();
    }

    private CodeBlock onContainerType(String container, ClassName innerClass, String name, String resultSetGetter, CodeBlock valueSetter) {

        if (container.equals(List.class.getName())) {
            container = ArrayList.class.getName();
        } else if (container.equals(Set.class.getName())) {
            container = HashSet.class.getName();
        }

        ClassName containerClass = getClass(container);
        String strValue = "list_" + name;
        CodeBlock.Builder code = CodeBlock.builder();
        if (resultSetGetter != null) {
            code.addStatement("$T<String> $L= (List<String>)parseList($L)", List.class, strValue, resultSetGetter);
        }
        ParameterizedTypeName classType = ParameterizedTypeName.get(containerClass, innerClass);
        code.addStatement("$T $L", classType, name)
                .beginControlFlow("if ($L == null)", strValue)
                .addStatement("$L = null", name)
                .nextControlFlow("else")
                .addStatement("$L = new $T()", name, classType) ///
                .beginControlFlow("for(int i = 0; i < $L.size(); i++)", strValue)
                .addStatement("String value = $L.get(i)", strValue)
                .addStatement("$L.add($L)", name, valueSetter)
                .endControlFlow()
                .endControlFlow();
        return code.build();
    }

    private ClassName getClass(String className) {
        String packageName = className.substring(0, className.lastIndexOf('.'));
        String simpleName = className.substring(className.lastIndexOf('.') + 1);
        return ClassName.get(packageName, simpleName);
    }


}


/*
	String _x = resultSet.getString(resultSet.findColumn("x"));
			List<String> list_x = (List<String>) parseList(_x);


			List<Object> x;
			if (list_x == null) {
				x = null;
			} else {
				x = new ArrayList<>();
				for(int i = 0; i < list_x.size(); i++) {
					String value = list_x.get(i);

				}
			}
 */

